﻿using System;
using System.Collections.Generic;
using System.Linq;
using Sigil.Impl;

namespace Sigil
{
	public partial class Emit<DelegateType>
	{
		private void LocalReleased(Local local)
		{
			FreedLocals.Add(local);

			CurrentLocals.Remove(local.Name);

			local.SetReleasedAt(IL.Index);
		}

		/// <summary>
		/// Declare a new local of the given type in the current method.
		///
		/// Name is optional, and only provided for debugging purposes.  It has no
		/// effect on emitted IL.
		///
		/// Be aware that each local takes some space on the stack, inefficient use of locals
		/// could lead to StackOverflowExceptions at runtime.
		/// 
		/// Jil will reuse local index on the stack if the corresponding Local instance has been disposed.
		/// By default Jil will set reused locals to their default value, you can change this behavior
		/// by passing initializeReused = false.
		/// </summary>
		public Local DeclareLocal<Type>(String name = null, Boolean initializeReused = true)
		{
			return DeclareLocal(typeof(Type), name, initializeReused);
		}

		/// <summary>
		/// Declare a new local of the given type in the current method.
		///
		/// Name is optional, and only provided for debugging purposes.  It has no
		/// effect on emitted IL.
		///
		/// Be aware that each local takes some space on the stack, inefficient use of locals
		/// could lead to StackOverflowExceptions at runtime.
		/// 
		/// Jil will reuse local index on the stack if the corresponding Local instance has been disposed.
		/// By default Jil will set reused locals to their default value, you can change this behavior
		/// by passing initializeReused = false.
		/// </summary>
		public Local DeclareLocal(Type type, String name = null, Boolean initializeReused = true)
		{
			if (type == null)
			{
				throw new ArgumentNullException("type");
			}

			name = name ?? AutoNamer.Next(this, "_local", Locals.Names, Labels.Names);

			if (CurrentLocals.ContainsKey(name))
			{
				throw new InvalidOperationException("Local with name '" + name + "' already exists");
			}

			var existingLocal = FreedLocals.FirstOrDefault(l => l.LocalType == type);

			DeclareLocallDelegate local;
			UInt16 localIndex;

			if (existingLocal == null)
			{
				local = IL.DeclareLocal(type);

				localIndex = NextLocalIndex;
				NextLocalIndex++;
			}
			else
			{
				local = existingLocal.LocalDel;
				localIndex = existingLocal.Index;

				FreedLocals.Remove(existingLocal);
			}

			var ret = new Local(this, localIndex, type, local, name, LocalReleased, IL.Index);

			UnusedLocals.Add(ret);

			AllLocals.Add(ret);

			CurrentLocals[ret.Name] = ret;

			// we need to initialize this local to it's default value, because otherwise
			//   it might be read from to get some non-sense
			if (existingLocal != null && initializeReused)
			{
				if (!type.IsValueType)
				{
					// reference types all get nulled
					LoadNull();
					StoreLocal(ret);
				}
				else
				{
					// handle known primitives better
					//   so as not to confuse the JIT
					var defaultLoaded = false;

					if (type == typeof(Boolean))
					{
						LoadConstant(default(Boolean));
						defaultLoaded = true;
					}

					if (type == typeof(Byte))
					{
						LoadConstant(default(Byte));
						defaultLoaded = true;
					}

					if (type == typeof(sbyte))
					{
						LoadConstant(default(sbyte));
						defaultLoaded = true;
					}

					if (type == typeof(Int16))
					{
						LoadConstant(default(Int16));
						defaultLoaded = true;
					}

					if (type == typeof(UInt16))
					{
						LoadConstant(default(UInt16));
						defaultLoaded = true;
					}

					if (type == typeof(Int32))
					{
						LoadConstant(default(Int32));
						defaultLoaded = true;
					}

					if (type == typeof(UInt32))
					{
						LoadConstant(default(UInt32));
						defaultLoaded = true;
					}

					if (type == typeof(Int64))
					{
						LoadConstant(default(Int64));
						defaultLoaded = true;
					}

					if (type == typeof(UInt64))
					{
						LoadConstant(default(UInt64));
						defaultLoaded = true;
					}

					if (type == typeof(Single))
					{
						LoadConstant(default(Single));
						defaultLoaded = true;
					}

					if (type == typeof(Double))
					{
						LoadConstant(default(Double));
						defaultLoaded = true;
					}

					if (defaultLoaded)
					{
						StoreLocal(ret);
					}
					else
					{
						// if it's use defined though, we've got little choice
						LoadLocalAddress(ret);
						InitializeObject(type);
					}
				}
			}

			return ret;
		}

		/// <summary>
		/// Declare a new local of the given type in the current method.
		///
		/// Name is optional, and only provided for debugging purposes.  It has no
		/// effect on emitted IL.
		///
		/// Be aware that each local takes some space on the stack, inefficient use of locals
		/// could lead to StackOverflowExceptions at runtime.
		/// 
		/// Jil will reuse local index on the stack if the corresponding Local instance has been disposed.
		/// By default Jil will set reused locals to their default value, you can change this behavior
		/// by passing initializeReused = false.
		/// </summary>
		public Emit<DelegateType> DeclareLocal<Type>(out Local local, String name = null, Boolean initializeReused = true)
		{
			return DeclareLocal(typeof(Type), out local, name, initializeReused);
		}

		/// <summary>
		/// Declare a new local of the given type in the current method.
		///
		/// Name is optional, and only provided for debugging purposes.  It has no
		/// effect on emitted IL.
		///
		/// Be aware that each local takes some space on the stack, inefficient use of locals
		/// could lead to StackOverflowExceptions at runtime.
		/// 
		/// Jil will reuse local index on the stack if the corresponding Local instance has been disposed.
		/// By default Jil will set reused locals to their default value, you can change this behavior
		/// by passing initializeReused = false.
		/// </summary>
		public Emit<DelegateType> DeclareLocal(Type type, out Local local, String name = null, Boolean initializeReused = true)
		{
			local = DeclareLocal(type, name, initializeReused);

			return this;
		}
	}
}